﻿//Copyright 2011 - Jonathan Allen

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Granite.Collections;
using Granite.Modeling.Internals;
using Granite.ComponentModel;
using Granite.Eventing;
using System.Diagnostics.Contracts;
using System.Diagnostics.CodeAnalysis;

namespace Granite.Modeling
{

	/// <summary>
	/// 
	/// </summary>
	/// <typeparam name="TModelType"></typeparam>
	public class EditableObjectModelCollection<TModelType> : ModelCollection<TModelType, EditableObjectPropertyBag>, IRevertibleChangeTracking, IChangeTrackingGraph, IEditableObject
	{
		readonly List<TModelType> m_OriginalList = new List<TModelType>();
		readonly List<TModelType> m_CheckpointItems = new List<TModelType>();
		bool m_AllowIsChangedEvents;
		bool m_IsChanged;
		bool m_IsChangedOld;

		void ConstuctorProcessing()
		{
			Properties.PropertyChanged += (s, e) => IsChanged = true;

			m_OriginalList.AddRange(this);

			ItemPropertyChanged += ChangeTrackingModelCollection_OnItemPropertyChanged;
			ItemAdded += ChangeTrackingModelCollection_ItemAdded;
			ItemRemoved += ChangeTrackingModelCollection_ItemRemoved;
			m_AllowIsChangedEvents = true;
		}

		void ChangeTrackingModelCollection_ItemRemoved(object sender, ItemEventArgs<TModelType> e)
		{
			if (m_AllowIsChangedEvents)
			{
				m_IsChanged = true;
				OnPropertyChanged("IsChanged");
			}
		}

		void ChangeTrackingModelCollection_ItemAdded(object sender, ItemEventArgs<TModelType> e)
		{
			if (m_AllowIsChangedEvents)
			{
				m_IsChanged = true;
				OnPropertyChanged("IsChanged");
			}
		}

		/// <summary>
		/// Creates a model by auto-constructing the property bag defined by TPropertyTracking.
		/// </summary>
		/// <remarks>Requires TPropertyTracking have a public constructor that accepts an Object</remarks>
		protected EditableObjectModelCollection()
		{
			ConstuctorProcessing();
		}

		/// <summary>
		/// Creates a model by auto-constructing the property bag defined by TPropertyTracking and populates it using the supplied collection
		/// </summary>
		/// <param name="list">The list from which the elements are copied.</param>
		/// <remarks>Requires TPropertyTracking have a public constructor that accepts an Object</remarks>
		protected EditableObjectModelCollection(List<TModelType> list)
			: base(list)
		{
			Contract.Requires(list != null);

			ConstuctorProcessing();
		}

		/// <summary>
		/// Creates a model by auto-constructing the property bag defined by TPropertyTracking and populates it using the supplied collection
		/// </summary>
		/// <param name="collection">The collection from which the elements are copied.</param>
		/// <remarks>Requires TPropertyTracking have a public constructor that accepts an Object</remarks>
		protected EditableObjectModelCollection(IEnumerable<TModelType> collection)
			: base(collection)
		{
			Contract.Requires(collection != null);

			ConstuctorProcessing();
		}

		/// <summary>
		/// Returns True if any fields were modified since the last call to Checkpoint. This also checks items that implement IChangeTracking.
		/// </summary>
		public bool IsChanged
		{
			get
			{
				return m_IsChanged;
			}
			protected set
			{
				if (m_IsChanged == value)
					return;
				m_IsChanged = value;
				OnPropertyChanged(CommonProperties.IsChangedProperty);
			}
		}

		/// <summary>
		/// Walk the object graph, looking for changed items in properties and the collection.
		/// </summary>
		/// <returns></returns>
		[Pure]
		public bool IsChangedGraph()
		{
			if (IsChanged)
				return true;

			//quick scan of child objects
			foreach (var item in this)
				if (item is IChangeTracking && ((IChangeTracking)item).IsChanged)
					return true;

			//deep scan of child objects
			foreach (var item in this.OfType<IChangeTrackingGraph>())
			{
				Contract.Assume(item != null);
				if (item.IsChangedGraph())
					return true;
			}

			//deep scan of properties
			return Properties.IsChangedGraph();
		}

		/// <summary>
		/// Marks all fields as unchanged and clears the IsChanged flag.
		/// </summary>
		/// <remarks>This will call AcceptChanges on properties and collection items that implement IChangeTracking</remarks>
		public void AcceptChanges()
		{
			Contract.Ensures(Properties.IsEditing == false);

			Properties.AcceptChanges(true);
			foreach (var item in this)
			{
				if (item is IChangeTracking)
					((IChangeTracking)item).AcceptChanges();
			}

			m_OriginalList.Clear();
			m_OriginalList.AddRange(this);
			m_CheckpointItems.Clear();

			IsChanged = false;

			Contract.Assume(Properties.IsEditing == false);
		}

		/// <summary>
		/// Discards all pending changes and reverts to the values used the last time AcceptChanges was called.
		/// </summary>
		/// <remarks>This will call RejectChanges on properties and collection items from the original collection that implement IRevertibleChangeTracking</remarks>
		public void RejectChanges()
		{
			Contract.Ensures(Properties.IsEditing == false);

			m_AllowIsChangedEvents = false;

			Clear();
			foreach (var item in m_OriginalList)
				Add(item);

			Properties.RejectChanges(true);
			foreach (var item in this)
			{
				if (item is IRevertibleChangeTracking)
					((IRevertibleChangeTracking)item).RejectChanges();
			}

			m_AllowIsChangedEvents = true;

			IsChanged = false;

			Contract.Assume(Properties.IsEditing == false);
		}

		void ChangeTrackingModelCollection_OnItemPropertyChanged(object sender, RelayedEventArgs<PropertyChangedEventArgs> e)
		{
			if (e.EventArgs.PropertyName == CommonProperties.IsChangedProperty.PropertyName)
				OnPropertyChanged(CommonProperties.IsChangedProperty);
		}

		[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
		[ContractInvariantMethod]
		void ObjectInvariant()
		{
			Contract.Invariant(m_OriginalList != null);
			Contract.Invariant(m_CheckpointItems != null);
		}

		/// <summary>
		/// Begins an edit on an object.
		/// </summary>
		public virtual void BeginEdit()
		{
			Contract.Ensures(Properties.IsEditing == true);

			if (Properties.IsEditing)
				return;

			Properties.BeginEdit();

			foreach (var item in this)
				m_CheckpointItems.Add(item);

			m_IsChangedOld = m_IsChanged;
			Contract.Assume(Properties.IsEditing == true);
		}

		/// <summary>
		/// Discards changes since the last <see cref="M:System.ComponentModel.IEditableObject.BeginEdit" /> call.
		/// </summary>
		public virtual void CancelEdit()
		{
			Contract.Ensures(Properties.IsEditing == false);

			Properties.CancelEdit();
			this.Clear();
			this.AddRange(m_CheckpointItems);
			m_CheckpointItems.Clear();
			
			m_IsChanged = m_IsChangedOld; //restore old IsChanged flag
			Contract.Assume(Properties.IsEditing == false);
		}

		/// <summary>
		/// Pushes changes since the last <see cref="M:System.ComponentModel.IEditableObject.BeginEdit" /> or <see cref="M:System.ComponentModel.IBindingList.AddNew" /> call into the underlying object.
		/// </summary>
		public virtual void EndEdit()
		{
			Contract.Ensures(Properties.IsEditing == false);

			Properties.EndEdit();
			m_CheckpointItems.Clear();

			Contract.Assume(Properties.IsEditing == false);
		}
	}
}
